/** 
  Copyright (c) 2008, 2009 Claudio Imbrenda
  This program is released under the terms of the
  GNU GPL version 3 or later.
*/
package macchina;
import java.io.*;
import javax.tools.JavaFileObject.Kind;
import javax.tools.*;
import java.util.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.DecimalFormat;
import java.text.FieldPosition;


/**
 * Utility class for ASCII <-> Baudot Code conversion.
 * @author the_nihilant
 */
public class MRUtils {
  
  /** 
   * Lookup table for ASCII to Baudot.
   */
  public static final byte[] atob ={
    0, 0, 0, 0, 0, 0, 0,26, 0, 0, 8, 0, 0, 2, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    4,26,22, 8, 5, 8,18,20,30, 9, 0,17, 6,24, 7,23,
   13,29,25,16,10, 1,21,28,12, 3,14, 6, 2,15, 2,19,
   31,24,19,14,18,16,22,11, 5,12,26,30, 9, 7, 6, 3,
   13,29,10,20, 1,28,15,25,23,21,17,30,23, 9,22,11,
   20,24,19,14,18,16,22,11, 5,12,26,30, 9, 7, 6, 3,
   13,29,10,20, 1,28,15,25,23,21,17,31,27, 7, 4, 0
  };
  
  /** Lookup table for Baudot (Letters) to ASCII.
   */
  public static final char[] b1toa={
    '*', 'T','<', 'O',' ','H','N','M',
    '\n','L','R', 'G','I','P','C','V',
    'E', 'Z','D', 'B','S','Y','F','X',
    'A', 'W','J', '|','U','Q','K','@'
  };
  
    /** Lookup table for Baudot (Ciphers) to ASCII.
   */
  public static final char[] b2toa={
    '*', '5', '<', '9', ' ', '\'',',', '.',
    '\n',')', '4', '%', '8', '0', ':', '=',
    '3', '+', '&', '?', '\'','6', '°', '/',
    '-', '2', '_', '|', '7', '1', '(', '@'   
  };
  
  
    /** Lookup table for Baudot (Letters) to historical representation.
   */
  public static final char[] b1toa_h={
    '*', 'T','\r','O',' ','H','N','M',
    '\n','L','R', 'G','I','P','C','V',
    'E', 'Z','D', 'B','S','Y','F','X',
    'A', 'W','J','\0','U','Q','K','\0'
  };
  
    /** Lookup table for Baudot (Ciphers) to historical representation.
   */
  public static final char[] b2toa_h={
    '*', '5', '\r','9', ' ', '\'',',', '.',
    '\n',')', '4', '%', '8', '0', ':', '=',
    '3', '+', '✠','?', '\'','6', '°', '/',
    '-', '2', '≠', '\0','7', '1', '(', '\0'   
  };
  
  /** ASCII representation of Baudot LTRS control character. */
  public static final char LTRS = '@';
  /** ASCII representation of Baudot CFRS control character. */
  public static final char CFRS = '|';
  /** ASCII representation of Baudot BELL control character. */
  public static final char BELL = '!';
  /** ASCII representation of Baudot POUND special character. */
  public static final char POUND = '$';
  /** ASCII representation of Baudot NOTEQ special character. */
  public static final char NOTEQ = '_';
  /** ASCII representation of Baudot DEG special character. */
  public static final char DEG = '"';  
  /** ASCII representation of Baudot NULL control character. */
  public static final char NULL = '*';
  /** ASCII representation of Baudot CR control character. */
  public static final char CR = '<';
  /** ASCII representation of Baudot LF control character. */
  public static final char LF = '%';
  /** ASCII representation of Baudot ESP control character. */
  public static final char ESP = ' ';
  
  /**
   * MRUtils is a pure static class
   */
  private MRUtils() {}
  
  /** Converts the given byte to ASCII, using LETTERS mode.
   *  Only the 5 least significant bits of b will be considered.
   *  This is equivalent to toASCII(b,true).
   * @param b the Baudot code to convert.
   * @return the character corresponding to the given Baudot character 
   *    (in LETTERS mode).
   */
  public static char toASCII(byte b){    
    return toASCII(b,true);
  }
  
  /** Converts the given byte to ASCII, using LETTERS mode or CIPHERS mode.
   *  Only the 5 least significant bits of b will be considered. if ltrs is
   *  true, LETTERS mode will be used, otherwise CIPHERS mode will be used.
   * @param b the Baudot code to convert.
   * @param ltrs indicates if LETTERS mode is to be used.
   * @return the character corresponding to the given Baudot character.
   */
  public static char toASCII(byte b, boolean ltrs){
    return (ltrs?b1toa:b2toa)[b&0x1f];
  }
  
  public static char toHistorical(byte b, boolean ltrs){
      return (ltrs?b1toa_h:b2toa_h)[b&0x1f];
  }
  
  /** Converts the given ASCII character to Baudot Code.
   *  This method operates only on standard 7-bit ASCII values, so only the 7 
   *  least significant bits of c will be considered.
   * @param c the character to convert to Baudot code.
   * @return the Baudot code for the given ASCII character.
   */
  public static byte toMR(char c){
    return atob[c&0x7f];
  }
  
  /**
   * Gets a bit in an int.
   * @param n the number to test.
   * @param b the bit index.
   * @return true if and only if the b-th bit of the number n is 1,
   * false otherwise.
   */
  public static boolean bit(int n,int b){
    int m=1<<b;
    return (n&m)!=0;
  }
  
  /**
   * Writes a word to an OutputStream. The 18-bit word is written as a 
   * little endian sequence of 4 bytes, representing its value. This means that
   * the last byte will always be 0.
   * @param o the OutputStream
   * @param w the word to be written. Only the 18 least significant bits will
   * be considered.
   * @throws java.io.IOException if an I/O exception occours.
   */
  public static void writeword(OutputStream o,int w)throws IOException{
    w=w&Macchina.DICIOTTO;
    for(int cx=0;cx<4;cx++,w>>=8)o.write(w&0xFF);
    o.flush();
  }
  
  
  /**
   * Method to infer the type of the String typed by the user.
   * Guesses if the string represents an integer (in base 2, 8, 10 or 16), 
   * or a 18-bit fixed-point value (as used by the Macchina Ridotta), or 
   * an instruction, according to the current configuration of the given
   * machine.
   * @param s String to guess
   * @param m an instance of Macchina, used for the configuration.
   * @param orig the original value.
   * @return an integer, which is the representation of the value of s.
   *    orig is returned in case of errors.
   * 
   */
  public static int understand(String s, Macchina m, int orig){
    s=s.trim().toLowerCase();
    
    try{
     if(s.toLowerCase().startsWith("0x"))
       return Integer.parseInt(s.substring(2), 16);
     if(s.toLowerCase().startsWith("0b"))
       return Integer.parseInt(s.substring(2), 2);
     if(s.toLowerCase().startsWith("0o"))
       return Integer.parseInt(s.substring(2), 8);
     return Integer.parseInt(s,10);
    }catch(NumberFormatException e){}
    

    try{
      double f = Double.parseDouble(s);
      if((f<-1)||(f>=1))return orig;
      return (int)(f*(1<<17)+(f<0?-0.5:0.5)); //+-0.5 to round to nearest
    }catch(NumberFormatException e){}
    
    if((s.length()==1)||((s.length()==2)&&(s.charAt(0)=='\''))||
            ((s.length()==3)&&s.charAt(0)=='\''&&s.charAt(2)=='\'')){
        return toMR(s.charAt(s.length()==1?0:1));
    }

    try{
      return Integer.parseInt(s, 16);
    }catch(NumberFormatException e){}
    
    
    try{
      int res;
      res= m.stringToOpcode((s.split(" "))[0]);
      res<<=10;
       // funzioncina per leggere i numeri in tante basi.
      int tmp=Integer.parseInt(s.split(" ")[1],10);
      if((tmp<0)||(tmp>=1024))return orig;
      res|=tmp;
      return res;
    }catch(NumberFormatException e){}
    catch(IllegalArgumentException e){}
    catch(IndexOutOfBoundsException e){}
    catch(NullPointerException e){}
    
    if(s.equals("cfrs"))return toMR(CFRS);
    if(s.equals("ltrs"))return toMR(LTRS);
    if(s.equals("bell"))return toMR(BELL);
    if(s.equals("null"))return toMR(NULL);
    if(s.equals("cr"))return toMR(CR);
    if(s.equals("lf"))return toMR(LF);
    
    return orig;
  }
  
  /**
   * Formats an int nicely, according to the given format.
   * Format specifiers are:<br/>
   * 0 for instructions<br/>
   * 1 for 18 bit fixed-point<br/>
   * 2 for base 2 integer<br/>
   * 3 for base 10 integer<br/>
   * 4 for base 16 integer<br/>
   * 5 for character<br/>
   * 6 for base 8 integer<br/>
   * @param i the integer to format
   * @param m an instance of Macchina, used for the configuration when
   *       instruction specification is specified. Can be null if format is not
   *       0.
   * @param format a format specifier, as specified above.
   * @param bits number of bits of the integer. Affects the zero-padding when
   * formatting a number in base 2, 8 or 16.
   * @return a String represeting the integer in the given format, or null if
   *        the format is invalid.
   */
  public static String niceformat(int i,Macchina m,int format, int bits){
    String res;
    String zeri;
    switch(format){
      case 0:
        zeri=m.getCurrentConfiguration();
        if(zeri==null)return null;
        res = m.getConfiguration(zeri)[(i>>10)&0x1f];
        return res==null?"-":((i&(~Macchina.QUINDICI))==0?"":"|")+
                res+" "+(i&0x3FF)+((i&(~Macchina.QUINDICI))==0?"":"|");
      case 1:
       return (new DecimalFormat("0.0000000")).format(
               ((double)(((i&(1<<17))==0?0:(~Macchina.DICIOTTO))
                |i))/((double)(1<<17)),new StringBuffer(), 
                new FieldPosition(0)).toString();
       
      case 2:
        res=Integer.toString(Macchina.DICIOTTO&i, 2);
        zeri="";
        for(int cx=0;cx<bits-res.length();cx++)zeri+="0";
        return "0b"+zeri+res;
      case 3:
        return Integer.toString(i);
      case 4:
        res=Integer.toHexString(Macchina.DICIOTTO&i).toUpperCase();
        zeri="";
        for(int cx=0;cx<(bits/4+((bits&3)==0?0:1))-res.length();cx++)zeri+="0";
        return "0x"+zeri+res;
      case 5:
        char a = toASCII((byte)i,false);
        char b = toASCII((byte)i,true);
        res = ""+a+((a!=b)?" / "+b:"");
        if((i&(~Macchina.CINQUE))!=0)res="| "+res+" |";
        return res;

      case 6:
        return Integer.toString((i>131071)?i-262144:i);

      case 8:
        res=Integer.toOctalString(Macchina.DICIOTTO&i).toUpperCase();
        zeri="";
        for(int cx=0;cx<(bits/3+(bits%3==0?0:1))-res.length();cx++)zeri+="0";
        return "0o"+zeri+res;  
      default:
        return null;
    }
  }
  
  public static String niceformat(int i,Macchina m,int format){
    return niceformat(i, m, format, 18);
  }
    
    
  /**
   * Name of the package for the instructions.
   */
  private static final String PACKAGEISTR = "macchina.istruzioni";
  
  /**
   * Template for the instructions. $name is a random name for the class,
   * $package will be the name of the package for the instructions, $code will
   * be the user code.
   */
  private static final String INSTRTEMPLATE= 
      "package $package;" +
      "import macchina.Macchinina;" +
      "import macchina.Logger;"+
      "public class $name implements macchina.Istruzione{" +
      " public byte execute(short ADDR, Macchinina MR,"+
                                "byte uinstr,Logger debug){" +
      "  $code \n" +
      " }\n" +
      "}\n";
  
  /**
   * List of forbidden strings. To avoid hijacking by the malicious user.
   */
  private static final String[] forblist = {"System","Class","Thread",
            "Compiler","Object","String","new ","Throwable","while","java.",
            "javax.", "macchina."};
  
  /**
   * Needed to generate different names for different instructions.
   */
  private static int instr_counter = 0;
  
  public static volatile boolean hasJDK = true;
  
  /**
   * Makes an uri from a string, catching the exception. 
   * Needed to improve readability of code, since it's used twice.
   * @param s the String to make an URI from.
   * @return an URI from the given String, or null in case of errors.
   */
  private static URI mkuri(String s){
    try{
      return new URI(s);
    }catch(URISyntaxException e){}
    return null;
  }
  
  /**
   * Compiles an instruction.
   * @param uinstrs the source code for the instruction to compile.
   * @return an instance of Instruction implementing the given instruction, 
   * or null in case of some errors
   * @throws macchina.SyntaxErrorException in case of a syntax error in the
   * source.
   */
  // welcome to hell
  public static synchronized Istruzione compileInstruction(String uinstrs)
                                                  throws SyntaxErrorException{
    //check against forbidden words.
    for(String forbidden:forblist){
      if(uinstrs.indexOf(forbidden)!=-1)return null;
    }
    /* 
     * names of the class and the source.
     * final because we read them from inner classes (and THEIR inner classes!).
     */
    // class name
    final String iname = "RUNTIME_"+(instr_counter++);
    // full qualified class name
    final String fname = PACKAGEISTR+"."+iname;
    // name of the binary file for the class
    final String filename = fname.replace('.','/')+".class";
    // instantiates the template
    final String c = INSTRTEMPLATE
                          .replace("$name", iname)
                          .replace("$package",PACKAGEISTR)
                          .replace("$code", uinstrs);
    
    //to collect compilation errors
    final DiagnosticCollector<JavaFileObject> errs = 
            new DiagnosticCollector<JavaFileObject>();
    // HACK! needed to pass a mutable (non-final) reference.
    final Pair<String,byte[]> pp = new Pair<String,byte[]>(c,null);
    // idem.
    final ByteArrayOutputStream bout = new ByteArrayOutputStream(1024);
    
    // source file
    //////////////////////////////////////////  
   final JavaFileObject  source = new SimpleJavaFileObject(mkuri(iname+".java"), 
              JavaFileObject.Kind.SOURCE){
      public CharSequence getCharContent(final boolean ig){
      if (pp.a == null)
         throw new UnsupportedOperationException("getCharContent()");
      /* 
       * the only source we might know is the source for the instruction, so
       * we don't event bother to check what's being asked for.
       */
      return pp.a;
      } 
    };
    //\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
    
    
    // the Java compiler, IF AVAILABLE :)
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//    compiler=null;  //to emulate the unavailability of the compiler.
    if(compiler==null)
        throw new SyntaxErrorException("Java Compiler NOT found!");
    
    //a custom classloader, to load a class which bytecode we have in an array.
    //////////////////////////////////////////
    final ClassLoader cl = new ClassLoader(){
      protected Class<?> findClass(final String qname)
                                          throws ClassNotFoundException {
        /* 
         * if the class being searched for is our class, make it from the 
         * bytecode we might have, otherwise just forward to the default 
         * class loader. 
         */
        if(qname.equals(fname))
          return defineClass(fname, pp.b,0,pp.b.length);
        return super.findClass(qname);
      }
      /*
       * if the class to be read is our class, just return a 
       * ByteArrayInputStream feeding from the bytecode we might have.
       * Otherwise forward the request to the default class loader.
       */
      public InputStream getResourceAsStream(final String w) {
        if(w.equals(filename))
                return new ByteArrayInputStream(pp.b);
        return super.getResourceAsStream(w);
      }
    };
    //\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
    
    // a default standard plain normal file manager.
    final JavaFileManager sfm = 
            compiler.getStandardFileManager(errs,null, null);
    
    //a custom JavaFileManager..
    //////////////////////////////////////////
    final JavaFileManager nf =
            new ForwardingJavaFileManager<JavaFileManager>(sfm){
      public ClassLoader getClassLoader(JavaFileManager.Location location) {
        //we just return OUR ClassLoader :)
        return cl;
      }
      
      public FileObject getFileForInput(Location l, String pn, String n) throws IOException {
        /* and since the ONLY file we want to compile is the source, we
         * again don't bother checking what's being asked for, and just return 
         * the source for the instruction.
         */
        return source;
      }
      public JavaFileObject getJavaFileForOutput(Location l, String fn,Kind k, 
                                          FileObject outf) throws IOException {
        // // // // // // // // // // // // // // // // // // // // //
        return new SimpleJavaFileObject(mkuri(iname+".class"), k){
          public InputStream openInputStream() {
            //since all output goes ONLY to bout, we just read from there.
            return new ByteArrayInputStream(bout.toByteArray());
          }
          
          public OutputStream openOutputStream() {
            /* all the output goes there, so we know where it is, and we expect
             * just one class file to be written anyway. If not so, everything
             * should just fail, so we can happily throw a SyntaxErrorException
             */
            return bout;
          }
        };
        // \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\
      }
    };
    //\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
    
    //the compiler WANTS an iterable..
    List<JavaFileObject> al = new ArrayList<JavaFileObject>();
    //so we just make a list of one element.
    al.add(source);
    
    /* we get a new CompilationTask with our FileManager, our
     * DiagnosticCollector, and our list of sources
     */ 
    JavaCompiler.CompilationTask task=
            compiler.getTask(null,nf,errs,null,null,al);
    
    //we can finally execute the compilation...
    Boolean esito = task.call();
    // thanks Sun for auto boxing/unboxing, allowing us this UGLY syntax:
    if((esito==null)||!esito){
      throw new SyntaxErrorException(errs);
    }
    
    /* we put the result of the compilation here, because here is where our
     * custom class loader expects it to be.
     */ 
    pp.b=bout.toByteArray();
    
    try{
      //now we just instantiate the newly compiled class..
      Class<?> neu = cl.loadClass(fname);
      /* obviously it has to be an instance of Istruzione, otherwise something 
       * went wrong somewhere. in that case we happily throw a 
       * SyntaxErrorException :)
       */
      Object n = neu.newInstance();
      return (Istruzione)n;
      
      // Exceptions meaning something wrong happened on our way here.
      // gotta catch'em all!      
    }catch(ClassNotFoundException e){
      String s="Syntax error!\n"; e.printStackTrace();
      throw new SyntaxErrorException(s);
    }catch(InstantiationException e){
      String s="Syntax error!\n"; e.printStackTrace();
      throw new SyntaxErrorException(s);
    }catch(IllegalAccessException e){
      String s="Syntax error!\n"; e.printStackTrace();
      throw new SyntaxErrorException(s);
    }catch(ClassCastException e){
      String s="Syntax error!\n"; e.printStackTrace();
      throw new SyntaxErrorException(s);
    }
  }
  
}
